/*
 * MicroEmulator 
 * Copyright (C) 2001 Bartek Teodorczyk <barteo@barteo.net>
 * 
 *  It is licensed under the following two licenses as alternatives:
 *    1. GNU Lesser General Public License (the "LGPL") version 2.1 or any newer version
 *    2. Apache License (the "AL") Version 2.0
 *
 *  You may not use this file except in compliance with at least one of
 *  the above two licenses.
 *
 *  You may obtain a copy of the LGPL at
 *      http://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt
 *
 *  You may obtain a copy of the AL at
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the LGPL or the AL for the specific language governing permissions and
 *  limitations.
 * 
 * @version $Id: J2SEInputMethod.java 12167 2009-03-27 11:29:59Z Michael.Busheikin $
 */

package org.microemu.device.j2se;

import java.awt.Toolkit;
import java.awt.datatransfer.StringSelection;
import java.awt.event.KeyEvent;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;
import java.util.Vector;

import javax.microedition.lcdui.Command;
import javax.microedition.lcdui.TextField;

import org.microemu.DisplayAccess;
import org.microemu.MIDletAccess;
import org.microemu.MIDletBridge;
import org.microemu.device.DeviceFactory;
import org.microemu.device.InputMethod;
import org.microemu.device.InputMethodEvent;
import org.microemu.device.InputMethodListener;
import org.microemu.device.impl.ButtonDetaultDeviceKeyCodes;
import org.microemu.device.impl.ButtonName;
import org.microemu.device.impl.InputMethodImpl;
import org.microemu.device.impl.SoftButton;
import org.microemu.log.Logger;
import org.microemu.util.ThreadUtils;

public class J2SEInputMethod extends InputMethodImpl {

	private boolean eventAlreadyConsumed;

	private Timer keyReleasedDelayTimer = null;

	private List repeatModeKeyCodes = new Vector();

	private class KeyReleasedDelayTask extends TimerTask {

		private int repeatModeKeyCode;

		KeyReleasedDelayTask(int repeatModeKeyCode) {
			this.repeatModeKeyCode = repeatModeKeyCode;

		}

		public void run() {
			if (repeatModeKeyCode != Integer.MIN_VALUE) {
				MIDletAccess ma = MIDletBridge.getMIDletAccess();
				if (ma == null) {
					return;
				}

				DisplayAccess da = ma.getDisplayAccess();
				if (da == null) {
					return;
				}

				da.keyReleased(repeatModeKeyCode);
				eventAlreadyConsumed = false;
				repeatModeKeyCode = Integer.MIN_VALUE;
			}
		}
	};

	
	/**
     * Schedule a new key-released task using the key-release timer.
     * 
     * BlueWhaleSystems fix: Ramesh Nair - 20 Nov 2007
	 * 
	 * This will attempt to schedule the given {@link KeyReleasedDelayTask}
	 * using {@link #keyReleasedDelayTimer}. If {@link IllegalStateException} 
	 * is thrown then it will reconstruct the timer and then try one more time.
	 * This fixes the issue whereby refreshing the applet in some browsers 
	 * causes the timer to get cancelled without then being reconstructed.
	 */
	private void scheduleKeyReleasedDelayTask(
	        KeyReleasedDelayTask task, long delay)
	{
	    try
	    {
	        keyReleasedDelayTimer.schedule( task, delay );
	    }
	    catch (IllegalStateException e)
	    {
	        // reconstruct timer
	        initKeyReleasedDelayTimer();
	        // try again
            keyReleasedDelayTimer.schedule( task, delay );
	    }
	}
	
	
    /**
     * Re-initialise the key-released delay timer.

     * BlueWhaleSystems fix: Ramesh Nair - 20 Nov 2007
     * 
     * We've refactored the timer construction code into this method so that 
     * we can re-initialise the timer whenever we want.
     */	
	private void initKeyReleasedDelayTimer()
	{
        // TODO When InputMethod will be removed from EmulatorContext add:
        // if (DeviceFactory.getDevice().hasRepeatEvents()) {
        keyReleasedDelayTimer = ThreadUtils.createTimer("InputKeyReleasedDelayTimer");
	}
	
	
	
	public J2SEInputMethod() {
		super();
		/**
		 * Re-initialise the key-released delay timer.

		 * BlueWhaleSystems fix: Ramesh Nair - 20 Nov 2007
		 * 
		 * We've refactored the timer construction code into this method so that 
		 * we can re-initialise the timer whenever we want.
		 */	
		initKeyReleasedDelayTimer();
	}

	/**
	 * Gets the game action associated with the given key code of the device.
	 * 
	 * @return the game action corresponding to this key, or <code>0</code> if
	 *         none
	 */
	public int getGameAction(int keyCode) {
		for (Iterator it = DeviceFactory.getDevice().getButtons().iterator(); it.hasNext();) {
			J2SEButton button = (J2SEButton) it.next();
			if (button.getKeyCode() == keyCode) {
				return ButtonDetaultDeviceKeyCodes.getGameAction(button.getFunctionalName());
			}
		}
		return 0;
	}

	/**
	 * 
	 * 
	 * @return a key code corresponding to this game action
	 * @throws IllegalArgumentException
	 *             if <code>gameAction</code> is not a valid game action
	 */
	public int getKeyCode(int gameAction) {
		ButtonName name = ButtonDetaultDeviceKeyCodes.getButtonNameByGameAction(gameAction);
		return J2SEDeviceButtonsHelper.getButton(name).getKeyCode();
	}

	/**
	 * @return a string name for the key
	 * @throws IllegalArgumentException
	 *             if <code>keyCode</code> is not a valid key code
	 */
	public String getKeyName(int keyCode) throws IllegalArgumentException {
		for (Iterator it = DeviceFactory.getDevice().getButtons().iterator(); it.hasNext();) {
			J2SEButton button = (J2SEButton) it.next();
			if (button.getKeyCode() == keyCode) {
				return button.getName();
			}
		}
		return Character.toString((char) keyCode);
	}

	protected boolean fireInputMethodListener(J2SEButton button, char keyChar, boolean shiftKeyIsDown) {
		MIDletAccess ma = MIDletBridge.getMIDletAccess();
		if (ma == null) {
			return false;
		}
		DisplayAccess da = ma.getDisplayAccess();
		if (da == null) {
			return false;
		}

		int keyCode = keyChar;
		if (button != null && keyChar == '\0') {
			keyCode = button.getKeyCode();
		}

		if (inputMethodListener == null) {
			da.keyPressed(keyCode);
			return true;
		}

		if (button == null) {
			return true;
		}

		ButtonName functionalName = button.getFunctionalName();

		/*
		 * BlueWhaleSystems fix: Ramesh Nair - 9 May 2008
		 */
        int caret = inputMethodListener.getCaretPosition();

        /**
         * BlueWhaleSystems fix: Ramesh Nair - 14-15 May 2008
         * 
         * Support HOME and END keys.
         */
        if (ButtonName.HOME.equals( functionalName ) || ButtonName.END.equals( functionalName ))
        {
            synchronized( this )
            {
                lastButton = null;
                lastButtonCharIndex = -1;
            }
            
            if (ButtonName.HOME.equals( functionalName ))
            {
                inputMethodListener.moveCaretToBeginningOfLine( shiftKeyIsDown );
            }
            else if (ButtonName.END.equals( functionalName ))
            {
                inputMethodListener.moveCaretToEndOfLine( shiftKeyIsDown );
            }

            return true;
        }
        
        
        if (ButtonName.UP.equals( functionalName ) || ButtonName.DOWN.equals( functionalName ))
		{
		    synchronized( this )
            {
	            lastButton = null;
	            lastButtonCharIndex = -1;
            }
		    
            /**
             * BlueWhaleSystems fix: Ramesh Nair - 12-15 May 2008
             * 
             * The input field may allow for moving the caret up a block of 
             * text. Lets try doing this before moving to another item on the 
             * screen. 
             */
	        // move up
	        if (ButtonName.UP.equals( functionalName ))
	        {
	            if (inputMethodListener.moveCaretUp(shiftKeyIsDown))
	            {
	                return true;
	            }
	        }
	        else if (ButtonName.DOWN.equals( functionalName ))
	        {
	            if (inputMethodListener.moveCaretDown(shiftKeyIsDown))
	            {
	                return true;
	            }
	        }

		    // fallback case is to move to next item on the screen
		    da.keyPressed(button.getKeyCode());
            return true;
		}


        if (ButtonName.LEFT.equals( functionalName ) || ButtonName.RIGHT.equals( functionalName ))
        {
            synchronized( this )
            {
                if (ButtonName.LEFT.equals( functionalName ))
                {
                    inputMethodListener.moveCaretLeft(shiftKeyIsDown);
                }
                else if (ButtonName.RIGHT.equals( functionalName ))
                {
                    inputMethodListener.moveCaretRight(shiftKeyIsDown);
                }
                
                lastButton = null;
                lastButtonCharIndex = -1;
            }
            
            return true;
        }		
		
		
		if (button.isModeChange()) {
			switch (inputMethodListener.getConstraints() & TextField.CONSTRAINT_MASK) {
			case TextField.ANY:
			case TextField.EMAILADDR:
			case TextField.URL:
			    setToNextRealInputMode();
				synchronized (this) {
					if (lastButton != null) {
						caret++;
						lastButton = null;
						lastButtonCharIndex = -1;
					}
				}
				InputMethodEvent event = new InputMethodEvent(InputMethodEvent.CARET_POSITION_CHANGED, caret,
						inputMethodListener.getText());
				inputMethodListener.caretPositionChanged(event);
				break;
			}
			return true;
		}


		if (ButtonName.BACK_SPACE.equals( functionalName ))
		{
            synchronized (this) {
                if (lastButton != null) {
                    caret++;
                    lastButton = null;
                    lastButtonCharIndex = -1;
                }
            }
		    
            /**
             * BlueWhaleSystems fix: Ramesh Nair - 14 Nov 2007
             * If we've highlighted some text then just delete it all
             */
		    if (null != inputMethodListener.getHighlightedText())
		    {
		        inputMethodListener.deleteHighlightedText();
		    }
		    else
		    {
	            String tmp = "";
	            
                if (caret > 0) {
                    caret--;
                    if (caret > 0) {
                        tmp += inputMethodListener.getText().substring(0, caret);
                    }
                    if (caret < inputMethodListener.getText().length() - 1) {
                        tmp += inputMethodListener.getText().substring(caret + 1);
                    }
                }

                if (!validate(tmp, inputMethodListener.getConstraints())) {
	                return true;
	            }
                
	            InputMethodEvent event = new InputMethodEvent(InputMethodEvent.INPUT_METHOD_TEXT_CHANGED, caret, tmp);
	            inputMethodListener.inputMethodTextChanged(event);
	            event = new InputMethodEvent(InputMethodEvent.CARET_POSITION_CHANGED, caret, tmp);
	            inputMethodListener.caretPositionChanged(event);
		    }
		    
			return true;
		}

		if (ButtonName.DELETE.equals( functionalName ))
		{
            synchronized (this) {
                if (lastButton != null) {
                    lastButton = null;
                    lastButtonCharIndex = -1;
                }
            }
            
            /**
             * BlueWhaleSystems fix: Ramesh Nair - 14 Nov 2007
             * If we've highlighted some text then just delete it all
             */
            if (null != inputMethodListener.getHighlightedText())
            {
                inputMethodListener.deleteHighlightedText();
            }
            else
            {
                String tmp = inputMethodListener.getText();
                if (caret != inputMethodListener.getText().length()) {
                    tmp = inputMethodListener.getText().substring(0, caret)
                            + inputMethodListener.getText().substring(caret + 1);
                }
                if (!validate(tmp, inputMethodListener.getConstraints())) {
                    return true;
                }
                InputMethodEvent event = new InputMethodEvent(InputMethodEvent.INPUT_METHOD_TEXT_CHANGED, caret, tmp);
                inputMethodListener.inputMethodTextChanged(event);
                event = new InputMethodEvent(InputMethodEvent.CARET_POSITION_CHANGED, caret, tmp);
                inputMethodListener.caretPositionChanged(event);
            }
            
			return true;
		}

		// SHIFT key on its own does nothing
		if (ButtonName.SHIFT.equals( functionalName ))
		{
            synchronized( this )
            {
                lastButton = null;
                lastButtonCharIndex = -1;
            }
            return true;
		}
		if (inputMethodListener.getText().length() < maxSize) {
			StringBuffer editText = new StringBuffer(inputMethodListener.getText());
			synchronized (this) {
				lastButtonCharIndex++;
				char[] buttonChars = filterConstraints(filterInputMode(button.getChars(getInputMode())));

				// if it's a valid character
				if ('\0' != keyChar && Character.MAX_VALUE != keyChar) {
					// Pass through letters and characters typed on keyboard but
					// not numbers that are buttons keys (presumably).

                    /**
                     * BlueWhaleSystems fix: Ramesh Nair - 14 May 2008
                     * 
                     * Get rid of highlighted text first
                     */
				    if (null != inputMethodListener.getHighlightedText())
				    {
	                    inputMethodListener.deleteHighlightedText();
	                    caret = inputMethodListener.getCaretPosition();
	                    editText = new StringBuffer(inputMethodListener.getText());
				    }
				    
                    if (insertOrAppendCharacter(editText, caret, keyChar, inputMethodListener.dontAllowNewLineChar()))
                    {
                        caret++;
                    }

					lastButton = null;
					lastButtonCharIndex = -1;
				} else if (buttonChars.length > 0) {
				    // if we've gone through all the characters on a given button
					if (lastButtonCharIndex >= buttonChars.length) {
					    // if there was only one character on the button then
					    // move the caret forward
						if (buttonChars.length == 1) {
							if (lastButton != null) {
								caret++;
							}
							lastButton = null;
						// if there is more than one character on the button 
						// then go back to the first character
						} else {
							lastButtonCharIndex = 0;
						}
					}
					// if we've pressed a different button to last
					if (lastButton != button) {
						if (lastButton != null) {
							caret++;
						}
						
						/**
					     * BlueWhaleSystems fix: Ramesh Nair - 14 May 2008
					     * 
					     * Get rid of highlighted text first
					     */
	                    if (null != inputMethodListener.getHighlightedText())
	                    {
	                        inputMethodListener.deleteHighlightedText();
	                        caret = inputMethodListener.getCaretPosition();
	                        editText = new StringBuffer(inputMethodListener.getText());
	                    }

						/* 
						 * BlueWhaleSystems fix: Ramesh Nair - 9,12 May 2008
						 */
						if (insertOrAppendCharacter(editText, caret, buttonChars[0], inputMethodListener.dontAllowNewLineChar()))

						lastButton = button;
						lastButtonCharIndex = 0;
					} else {
					    // cycling through the characters on a given button
						editText.setCharAt(caret, buttonChars[lastButtonCharIndex]);
						lastButton = button;
					}
				} else {
					lastButton = null;
					lastButtonCharIndex = -1;
				}
				resetKey = false;
				notify();
			}
			if (!validate(editText.toString(), inputMethodListener.getConstraints())) {
				return false;
			}
			InputMethodEvent event = new InputMethodEvent(InputMethodEvent.INPUT_METHOD_TEXT_CHANGED, caret, editText
					.toString());
			inputMethodListener.inputMethodTextChanged(event);
		}
		return false;
	}

	
    /**
     * BlueWhaleSystems fix: Ramesh Nair - 9 May 2008
     * 
     * Not all input fields allow for newline characters to be inserted. Also 
     * we separate out the insertion logic into this method for efficiency 
     * and maintainability reasons.
     * 
     * @return true if character was inserted; false otherwise.
     */     	
	private boolean insertOrAppendCharacter(StringBuffer text, int caretPosition, char charToAppend, boolean dontAppendNewLineChar)
	{
        /**
         * BlueWhaleSystems fix: Ramesh Nair - 14 Nov 2007
         * 
         * Fix for issue whereby you were unable to insert new 
         * characters into the middle of an existing text string. 
         * No matter where the caret was positioned new characters 
         * were always being appended onto the end.
         */             
	    
	    if ( !(dontAppendNewLineChar && '\n' == charToAppend) )
	    {
	        // appending onto end?
	        if (text.length() < caretPosition) {
	            text.append(charToAppend);
	        // inserting in place?
	        } else {
	            text.insert(caretPosition, charToAppend);
	        }       
	        
	        return true;
	    }
	    
	    return false;
	}
	
	
	
	public void buttonTyped(J2SEButton button) {
		if (eventAlreadyConsumed) {
			return;
		}
	}

	public void pasteAtCursor(String str)
	{
		/* 
		 * BlueWhaleSystems fix: Ramesh Nair - 19 May 2008
		 */
		if (null != inputMethodListener && null != inputMethodListener.getText())
		{
		    // get length of current content
		    int lengthOfExistingText = inputMethodListener.getText().length();
		    // subtract length of highlighted content
		    String highlightedText = inputMethodListener.getHighlightedText();
		    if (null != highlightedText)
		    {
		        lengthOfExistingText -= highlightedText.length();
		    }
		    
		    // now ensure that length after pasting doesn't exceed max length
		    if (lengthOfExistingText + str.length() <= maxSize)
		    {
	            inputMethodListener.deleteHighlightedText();
	            
	            /**
	             * BlueWhaleSystems fix: Ramesh Nair - 13 May 2008
	             * Don't paste in newline characters where they aren't allowed
	             */         
	            if (inputMethodListener.dontAllowNewLineChar())
	            {
	                str = str.replaceAll( "[\\n\\r]+", "" );
	            }
	            
	            insertText(str);
		    }
		}
		eventAlreadyConsumed = true;
	}
	
	
	
    /**
     * Higlight all the text in the current input field.
     * BlueWhaleSystems fix: Ramesh Nair - 19 May 2008
     */	
	public void highlightAllText()
	{
        if (null != inputMethodListener)
        {
            inputMethodListener.highlightAllText();
        }	    
	}
	
	
	/**
	 * Get current highlighted text.
     * BlueWhaleSystems fix: Ramesh Nair - 14 May 2008
     * @return null if no text is currently highlighted.
	 */
    public String getHighlightedText()
    {
        if (null != inputMethodListener)
        {
            return (inputMethodListener.getHighlightedText());
        }
        return null;
    }
    
    
    
    /**
     * Delete current highlighted text.
     * BlueWhaleSystems fix: Ramesh Nair - 14 May 2008
     */
    public void deleteHighlightedText()
    {
        if (null != inputMethodListener)
        {
            inputMethodListener.deleteHighlightedText();
        }
    }    
	

	public void buttonPressed(J2SEButton button, char keyChar, boolean shiftKeyIsDown) {
		int keyCode = keyChar;
		if (button != null && keyChar == '\0') {
			keyCode = button.getKeyCode();
		}
		eventAlreadyConsumed = false;
		if (DeviceFactory.getDevice().hasRepeatEvents()) {
			if (repeatModeKeyCodes.contains(new Integer(keyCode))) {
				MIDletAccess ma = MIDletBridge.getMIDletAccess();
				if (ma == null) {
					return;
				}
				DisplayAccess da = ma.getDisplayAccess();
				if (da == null) {
					return;
				}
				da.keyRepeated(keyCode);
				eventAlreadyConsumed = true;
				return;
			} else {
				repeatModeKeyCodes.add(new Integer(keyCode));
			}
		}

        /**
         * BlueWhaleSystems fix: Ramesh Nair - 26 Dec 2007
         * 
         * Soft buttons should work even when in full-screen mode as this is 
         * how midlets behave on most devices.
         */
		// invoke any associated commands, but send the raw key codes instead
		if (button instanceof SoftButton) {
			Command cmd = ((SoftButton) button).getCommand();
			if (cmd != null) {
				MIDletAccess ma = MIDletBridge.getMIDletAccess();
				if (ma == null) {
					return;
				}
				DisplayAccess da = ma.getDisplayAccess();
				if (da == null) {
					return;
				}
				da.commandAction(cmd, da.getCurrent());
				eventAlreadyConsumed = true;
				return;
			}
		}

		if (fireInputMethodListener(button, keyChar, shiftKeyIsDown)) {
			eventAlreadyConsumed = true;
			return;
		}
	}

	public void buttonReleased(J2SEButton button, char keyChar, boolean shiftKeyIsDown) {
		int keyCode = keyChar;
		if (button != null && keyChar == '\0') {
			keyCode = button.getKeyCode();
		}

		/**
		 * BlueWhaleSystems fix: Ramesh Nair - 04 Dec 2007
		 * Makes sure we remove the keycode from the 
		 * "currently-repeated-key-list" regardless of whether a component 
		 * (e.g. a TextField) has set its own InputMethodListener.
		 */
		repeatModeKeyCodes.remove(new Integer(keyCode));

		if (DeviceFactory.getDevice().hasRepeatEvents()) {
			/**
			 * BlueWhaleSystems fix: Ramesh Nair - 20 Nov 2007
			 * see scheduleKeyReleasedDelayTask().
			 *    Michael Busheikin - 3 Apr 2009
			 * Sending in the keycode of the button was suggesting that when a non-number keyboard
			 * char was typed then a different key was released than was pressed.
			 * See ticket:3080
			 */
			scheduleKeyReleasedDelayTask( new KeyReleasedDelayTask(keyCode), 50 );
		} else {
			MIDletAccess ma = MIDletBridge.getMIDletAccess();
			if (ma == null) {
				return;
			}

			DisplayAccess da = ma.getDisplayAccess();
			if (da == null) {
				return;
			}

			if (null != inputMethodListener)
			{
	            // shift key released?
	            if (ButtonName.SHIFT.equals( button.getFunctionalName() ) && 
	                    !shiftKeyIsDown)
	            {
	                
	            }
			}
			
			da.keyReleased(keyCode);
			eventAlreadyConsumed = false;
		}
	}

	public J2SEButton getButton(KeyEvent ev) {
		J2SEButton button = J2SEDeviceButtonsHelper.getButton(ev);
		if (button != null) {
			return button;
		}
		
        /**
         * BlueWhaleSystems fix: Ramesh Nair - 9 May 2008
         * Remove unnecessary check
         * 
         * fix: Michael Busheikin - 3 Apr 2009
         * (Removed if statement around button iteration.)
         * Remove another unnecessary check - the INPUT_ constants are only used for microemu's midp 
         * implementation, so with something like LWUIT which doesn't use it this block wasn't executing.
         * See ticket:3080
         */		
		for (Enumeration e = DeviceFactory.getDevice().getButtons().elements(); e.hasMoreElements();) {
			button = (J2SEButton) e.nextElement();
			if (button.isChar(ev.getKeyChar(), getInputMode())) {
				return button;
			}
		}
		return null;
	}
}
